/*
 * Copyright 2021 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.camera.video.internal;

import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.camera.core.Logger;
import androidx.core.util.Pair;
import androidx.core.util.Preconditions;

import java.io.Closeable;
import java.nio.ByteBuffer;
import java.util.Locale;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * A read-only wrapper for a {@link ByteBuffer} that may be shared between multiple clients.
 *
 * <p>Every {@code SharedByteBuffer} must be closed with {@link #close()} when no longer in use.
 * This will signal to other clients that it may be safe to reclaim or reuse the wrapped ByteBuffer.
 * Failure to call {@link #close()} may lead to memory leaks or create stalls in processing
 * pipelines.
 *
 * <p>The {@link ByteBuffer} returned by {@link #get()} is safe to read from as long as
 * {@link #close()} has not been called. Once {@link #close()} has been called, it should be
 * assumed the {@link ByteBuffer} contains invalid data.
 */
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
public final class SharedByteBuffer implements Closeable {

    private static final String TAG = "SharedByteBuffer";

    private final ByteBuffer mSharedByteBuffer;

    // Unique ID for the original SharedByteBuffer. Transferred to all share() instances.
    private final int mShareId;

    private final Object mCloseLock = new Object();

    private final Pair<Executor, Runnable> mFinalCloseAction;

    @GuardedBy("mCloseLock")
    private final AtomicInteger mSharedRefCount;

    @GuardedBy("mCloseLock")
    private boolean mClosed = false;

    /**
     * Creates an initial instance of SharedByteBuffer for sharing a {@link ByteBuffer} between
     * multiple clients.
     *
     * <p>The initial SharedByteBuffer holds the initial reference count. Each subsequent call to
     * {@link #share()} will increment the reference count. Once all instances generated by
     * {@link #share()} have been closed, and the initial instance has also been closed, the
     * provided {@code finalCloseAction} will run.
     *
     * @param sharedBuf The buffer to be wrapped.
     * @param closeActionExecutor The executor used to execute {@code finalCloseAction}.
     * @param finalCloseAction The action to take once all shared instances have been closed.
     *                         This action will only be run once the initial instance and all
     *                         instances generated by {@link #share()} have been closed. A common
     *                         {@code finalCloseAction} might be to return the ByteBuffer to a
     *                         pool so it can be reused.
     */
    @NonNull
    static SharedByteBuffer newSharedInstance(@NonNull ByteBuffer sharedBuf,
            @NonNull Executor closeActionExecutor, @NonNull Runnable finalCloseAction) {
        AtomicInteger sharedRefCount = new AtomicInteger(1);
        int shareId = System.identityHashCode(sharedBuf);
        ByteBuffer readOnlyBuf = Preconditions.checkNotNull(sharedBuf).asReadOnlyBuffer();
        return new SharedByteBuffer(readOnlyBuf, sharedRefCount,
                new Pair<>(Preconditions.checkNotNull(closeActionExecutor),
                        Preconditions.checkNotNull(finalCloseAction)), shareId);
    }

    /**
     * Creates a new instance of a SharedByteBuffer from an existing ByteBuffer and ref count.
     *
     * <p>This constructor must always be used with a pre-incremented {@code sharedRefCount}.
     */
    private SharedByteBuffer(@NonNull ByteBuffer sharedBuf, @NonNull AtomicInteger sharedRefCount,
            @NonNull Pair<Executor, Runnable> finalCloseAction, int shareId) {

        mSharedByteBuffer = sharedBuf;
        mSharedRefCount = sharedRefCount;
        mFinalCloseAction = finalCloseAction;
        mShareId = shareId;

        if (Logger.isDebugEnabled(TAG)) {
            int refCount = sharedRefCount.get();
            if (refCount < 1) {
                throw new AssertionError(String.format(Locale.US, "Cannot create new "
                        + "instance of SharedByteBuffer with invalid ref count %d. Ref count must "
                        + "be >= 1. [%s]", refCount, toString()));
            }
        }
    }

    /**
     * Creates a new instance of SharedByteBuffer which references the same underlying ByteBuffer.
     *
     * <p>This creates a new read-only instance of the same underlying {@link ByteBuffer} that
     * can be retrieved with {@link #get()}. While the two instances are independent, the new
     * instance's initial position, limit, and mark values will match those of this instance's
     * values at the time {@code share()} is called.
     */
    @NonNull
    SharedByteBuffer share() {
        int newRefCount;
        AtomicInteger sharedRefCountLocal;
        synchronized (mCloseLock) {
            checkNotClosed("share()");
            newRefCount = mSharedRefCount.incrementAndGet();
            sharedRefCountLocal = mSharedRefCount;
        }

        if (Logger.isDebugEnabled(TAG)) {
            if (newRefCount <= 1) {
                throw new AssertionError("Invalid ref count. share() should always produce "
                        + "a ref count of 2 or more.");
            }
            Logger.d(TAG, String.format(Locale.US, "Ref count incremented: %d [%s]",
                    newRefCount, toString()));
        }

        return new SharedByteBuffer(mSharedByteBuffer.asReadOnlyBuffer(), sharedRefCountLocal,
                mFinalCloseAction, mShareId);
    }

    /**
     * Closes this instance of SharedByteBuffer.
     *
     * <p>Once an instance is closed, {@link #get()} can no longer be used to retrieve the
     * buffer. If any reference to the buffer exists from a previous invocation of {@link #get()}
     * it should no longer be used.
     *
     * <p>Closing a SharedByteBuffer is idempotent. Only the first {@code close()} will have any
     * effect. Subsequent calls will be no-ops.
     *
     */
    @Override
    public void close() {
        closeInternal();
    }

    /**
     * Returns a read-only view into the {@link ByteBuffer} instance that is being shared.
     *
     *  <p>Once an instance is closed with {@link #close()}, this method can no longer be used to
     *  retrieve the buffer. Once closed, the object returned by this method should be assumed
     *  invalid and should no longer be used.
     *
     * @return the underlying shared {@link ByteBuffer}
     * @throws IllegalStateException if this SharedByteBuffer is closed.
     * @see #close()
     */
    @NonNull
    public ByteBuffer get() {
        synchronized (mCloseLock) {
            checkNotClosed("get()");
            return mSharedByteBuffer;
        }
    }

    @GuardedBy("mCloseLock")
    private void checkNotClosed(@NonNull String caller) {
        if (mClosed) {
            throw new IllegalStateException("Cannot call " + caller + " on a closed "
                    + "SharedByteBuffer.");
        }
    }

    @NonNull
    @Override
    public String toString() {
        return String.format(Locale.US, "SharedByteBuffer[buf: %s, shareId: 0x%x, instanceId:0x%x]",
                mSharedByteBuffer, mShareId, System.identityHashCode(this));
    }

    // Finalizer as a safety net. This will introduce a performance penalty on construction but it
    // is often critical that all SharedByteBuffer instances are closed, so we eat the penalty.
    @Override
    protected void finalize() throws Throwable {
        try {
            if (closeInternal()) {
                Logger.w(TAG, String.format(Locale.US,
                        "SharedByteBuffer closed by finalizer, but should have been closed "
                                + "manually with SharedByteBuffer.close() [%s]",
                        toString()));
            }
        } finally {
            super.finalize();
        }

    }

    // Closes the SharedByteBuffer and returns true if the buffer was not already closed. Returns
    // false if the SharedByteBuffer was already closed.
    private boolean closeInternal() {
        int newRefCount;
        synchronized (mCloseLock) {
            if (mClosed) {
                // close() is idempotent. Only initial close() should have any affect on ref count.
                return false;
            } else {
                mClosed = true;
            }
            newRefCount = mSharedRefCount.decrementAndGet();
        }

        if (Logger.isDebugEnabled(TAG)) {
            if (newRefCount < 0) {
                throw new AssertionError("Invalid ref count. close() should never produce a "
                        + "ref count below 0");
            }
            // Note there is no guarantee this log will be printed in order with other shared
            // instances log since there is no mutual exclusion around when logs are printed.
            // This may make it appear as if ref counts are being decremented out of order, but
            // that is not the case.
            Logger.d(TAG, String.format(Locale.US, "Ref count decremented: %d [%s]", newRefCount,
                    toString()));
        }

        if (newRefCount == 0) {
            if (Logger.isDebugEnabled(TAG)) {
                Logger.d(TAG, String.format(Locale.US, "Final reference released. Running final "
                        + "close action. [%s]", toString()));
            }

            try {
                Executor executor = Preconditions.checkNotNull(mFinalCloseAction.first);
                Runnable runnable = Preconditions.checkNotNull(mFinalCloseAction.second);
                executor.execute(runnable);
            } catch (RejectedExecutionException e) {
                Logger.e(TAG, String.format(Locale.US, "Unable to execute final close action. "
                                + "[%s]", toString()), e);
            }
        }

        return true;
    }
}
